什么是Runtime
Objective-C语言尽可能地将一些决定从编译时推迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。Objective-C Runtime系统是整个Objective-C语言的基石,它使Objective-C的动态特性成为可能。
与Runtime交互
Objc从三种不同的层级上与Runtime系统进行交互,分别是通过Objective-C源代码
,通过 Foundation框架的NSObject类定义的方法
,通过对runtime函数
的直接调用。
Objective-C源代码
大部分情况下你就只管写你的Objc代码就行,runtime系统自动在幕后辛勤劳作着。当编译OC类和方法时,编译器会根据类和分类中的定义为我们建立相应的数据结构和方法调用。
NSObject方法
NSObject类是OC类中基类。NSObject类的description
方法起到了抽象接口的作用,任何继承了NSObject的子类都可以重写本方法返回更多关于子类自身的信息。一些 NSObject方法可以向runtime system 询问一些信息,这些方法使得对象可以自省
(self introspection),比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。
Runtime函数
Runtime系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc
目录下。
Runtime相关数据结构
有关数据结构的定义在/usr/include/objc
及/usr/include/objc/runtime.h
目录下可以找到。
SEL
SEL代表一个方法选择器,可以理解为区分方法的ID
或方法名字
,其实它就是个映射到方法的C字符串
,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。它的定义如下:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
id
id是指向类实例的指针(A pointer to an instance of a class), 即任何对象指针。
typedef struct objc_object *id;
objc_object结构体定义如下,它代表类的实例,即对象。它包含一个指向实例所属类的isa指针。
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
Class
Class是一个指向objc_class结构体的指针:
typedef struct objc_class *Class;
objc_class结构体代表一个类,它的定义如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
啊,一堆东西啊!光看名字就知道,super_class是父类,name是类名,instance_size是实例所占字节数,ivars实例变量,methodLists方法列表,cache方法缓存,protocols遵守的协议。
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
objc_ivar_list结构体存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list结构体存储着objc_method数组列表,而objc_method结构体存储了类的某个方法的信息。
OC中可以给类发送消息,比如[NSObject alloc],因为OC中类也是对象,那么类对象的类型是什么呢?答案就是元类
(Meta Class),类对象所属类型就叫做元类,它用来定义类方法。所以当 [NSObject alloc] 这条消息发给类对象的时候,会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
OC Runtime类结构图:
从图中可以看出:一个类对象只有一个与之相对应的元类,根元类的父类是NSObject,而NSObject的父类为nil,也就是它没有父类,所有元类的isa都指向根元类(根元类isa指向自己)。
Method
Method代表类中的某个方法。
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}OBJC2_UNAVAILABLE
objc_method结构体定义了方法名,方法类型(参数类型和返回值类型),方法实现(函数指针)。
Ivar
Ivar是一种代表类中实例变量的类型。
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name //实例变量名字 OBJC2_UNAVAILABLE;
char *ivar_type //实例变量类型 OBJC2_UNAVAILABLE;
int ivar_offset //内存偏移 OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}OBJC2_UNAVAILABLE;
IMP
IMP是方法实现。
typedef id (*IMP)(id, SEL, ...);
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。
Cache
在runtime.h中Cache的定义如下:
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
Property
@property标记了类中的属性,它是一个指向objc_property结构体的指针。
typedef struct objc_property *objc_property_t;
可以通过class_copyPropertyList 和 protocol_copyPropertyList方法来获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
Category
Category代表分类,对应着objc_category结构体。
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
// objc-runtime-new.h中定义
struct category_t {
const char *name; // name 是指 class_name 而不是category_name
classref_t cls; // cls是要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties; // instanceProperties表示Category里所有的properties,(这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,)不过这个和一般的实例变量是不一样的
;
category就是定义方法的结构体,instance_methods列表是objc_class中方法列表的一个子集,class_methods列表是元类方法列表的一个子集。由其结构成员可知,category为什么不能添加成员变量(可添加属性,只有set/get方法)。
给category添加方法后,category_list会生成method list。这个方法列表是倒序添加的,也就是说,新生成的category的方法会先于旧的category的方法插入。(category的方法会优先于类方法执行)。
super
super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用方法时,跳过当前类去调用父类的方法,而不是本类中的方法。self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。实际上给super发消息时,super还是与self指向的是相同的消息接收者。
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
原理:使用super来接收消息时,编译器会生成一个objc_super结构体。发送消息时,不是调用objc_msgSend函数,而是调用objc_msgSendSuper函数:
id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );
该函数实际的操作是:从objc_super结构体指向的superClass的方法列表开始查找selector,找到后以objc->receiver去调用这个selector。
Runtime开源源码对一些方法的实现:
- (Class)class ;
- (Class)class {
return object_getClass(self);
}
+ (Class)class;
+ (Class)class {
return self;
}
- (BOOL)isKindOf:aClass;// (for循环遍历父类,每次判断返回的结果可能不同)
- (BOOL)isKindOf:aClass
{
Class cls;
for (cls = isa; cls; cls = cls->superclass)
if (cls == (Class)aClass)
return YES;
return NO;
}
- (BOOL)isMemberOf:aClass;
- (BOOL)isMemberOf:aClass
{
return isa == (Class)aClass;
}
消息
objc_msgSend函数
在OC中,消息直到运行时才绑定方法实现,这叫做动态绑定(dynamic binding)。编译器将oc中的消息表达式
[target message:arg1]
转换为:
objc_msgSend(target, selector, arg1, arg2, ...)
消息函数objc_msgSend实现动态绑定的过程:
- 首先根据target所属的类和selector找到对应唯一的程序(procedure),即方法实现imp;
- 调用方法实现imp,target、arg1等作为参数传给该方法。
- 消息函数返回imp(…)的返回值。
更为详细的过程是:(以下内容来自yulingtianxia)
1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
4. 如果 cache 找不到就找一下方法分发表。
5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
6. 如果还找不到就要开始进入动态方法解析了,后面会提到。
PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地址联系起来。
OC消息框架图:
使用隐藏参数
当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
- 接收消息的对象(也就是self指向的内容)
- 方法选择器(_cmd指向的内容)
之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self引用了接收者对象,而_cmd引用了方法本身的选择器:
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
获取方法地址
消息动态绑定的过程会消耗部分时间,如何避开消息的动态绑定过程呢?答案就是获取selector对应的方法地址直接调用
。当一个特定的方法将要执行许多次并且你想避免方法每次执行时动态绑定的耗时时,这种方式可能较为合适。利用NSObject的methodForSelector:
方法可以获取selector对应的方法实现指针,经过类型转换之后可以直接调用。
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
ps:方法实现以函数形式直接调用时,self
和_cmd
必须作为前2个参数显示给出。methodForSelector:方法是由 Cocoa 的 Runtime 系统提供的,而不是 Objc 自身的特性。
官方原文:Objective-C Runtime Programming Guide
中文翻译:http://blog.csdn.net/iosswift/article/details/42245647
http://southpeak.github.io/blog/2014/10/25/objective-c-runtime-yun-xing-shi-zhi-lei-yu-dui-xiang/